{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Week 4 Day 4 - preparing the big project!\n",
    "\n",
    "# The Sidekick\n",
    "\n",
    "It's time to introduce:\n",
    "\n",
    "1. Structured Outputs\n",
    "2. A multi-agent flow"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Annotated, TypedDict, List, Dict, Any, Optional\n",
    "from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_community.agent_toolkits import PlayWrightBrowserToolkit\n",
    "from langchain_community.tools.playwright.utils import create_async_playwright_browser\n",
    "from langgraph.graph import StateGraph, START, END\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langgraph.prebuilt import ToolNode\n",
    "from langgraph.graph.message import add_messages\n",
    "from pydantic import BaseModel, Field\n",
    "from IPython.display import Image, display\n",
    "import gradio as gr\n",
    "import uuid\n",
    "from dotenv import load_dotenv"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "load_dotenv(override=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### For structured outputs, we define a Pydantic object for the Schema"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# First define a structured output\n",
    "\n",
    "class EvaluatorOutput(BaseModel):\n",
    "    feedback: str = Field(description=\"Feedback on the assistant's response\")\n",
    "    success_criteria_met: bool = Field(description=\"Whether the success criteria have been met\")\n",
    "    user_input_needed: bool = Field(description=\"True if more input is needed from the user, or clarifications, or the assistant is stuck\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### And for the State, we'll use TypedDict again\n",
    "\n",
    "But now we have some real information to maintain!\n",
    "\n",
    "The messages uses the reducer. The others are simply values that we overwrite with any state change."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# The state\n",
    "\n",
    "class State(TypedDict):\n",
    "    messages: Annotated[List[Any], add_messages]\n",
    "    success_criteria: str\n",
    "    feedback_on_work: Optional[str]\n",
    "    success_criteria_met: bool\n",
    "    user_input_needed: bool"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get our async Playwright tools\n",
    "# If you get a NotImplementedError here or later, see the Heads Up at the top of the 3_lab3 notebook\n",
    "\n",
    "\n",
    "import nest_asyncio\n",
    "nest_asyncio.apply()\n",
    "async_browser =  create_async_playwright_browser(headless=False)  # headful mode\n",
    "toolkit = PlayWrightBrowserToolkit.from_browser(async_browser=async_browser)\n",
    "tools = toolkit.get_tools()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Initialize the LLMs\n",
    "\n",
    "worker_llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
    "worker_llm_with_tools = worker_llm.bind_tools(tools)\n",
    "\n",
    "evaluator_llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
    "evaluator_llm_with_output = evaluator_llm.with_structured_output(EvaluatorOutput)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# The worker node\n",
    "\n",
    "def worker(state: State) -> Dict[str, Any]:\n",
    "    system_message = f\"\"\"You are a helpful assistant that can use tools to complete tasks.\n",
    "You keep working on a task until either you have a question or clarification for the user, or the success criteria is met.\n",
    "This is the success criteria:\n",
    "{state['success_criteria']}\n",
    "You should reply either with a question for the user about this assignment, or with your final response.\n",
    "If you have a question for the user, you need to reply by clearly stating your question. An example might be:\n",
    "\n",
    "Question: please clarify whether you want a summary or a detailed answer\n",
    "\n",
    "If you've finished, reply with the final answer, and don't ask a question; simply reply with the answer.\n",
    "\"\"\"\n",
    "    \n",
    "    if state.get(\"feedback_on_work\"):\n",
    "        system_message += f\"\"\"\n",
    "Previously you thought you completed the assignment, but your reply was rejected because the success criteria was not met.\n",
    "Here is the feedback on why this was rejected:\n",
    "{state['feedback_on_work']}\n",
    "With this feedback, please continue the assignment, ensuring that you meet the success criteria or have a question for the user.\"\"\"\n",
    "    \n",
    "    # Add in the system message\n",
    "\n",
    "    found_system_message = False\n",
    "    messages = state[\"messages\"]\n",
    "    for message in messages:\n",
    "        if isinstance(message, SystemMessage):\n",
    "            message.content = system_message\n",
    "            found_system_message = True\n",
    "    \n",
    "    if not found_system_message:\n",
    "        messages = [SystemMessage(content=system_message)] + messages\n",
    "    \n",
    "    # Invoke the LLM with tools\n",
    "    response = worker_llm_with_tools.invoke(messages)\n",
    "    \n",
    "    # Return updated state\n",
    "    return {\n",
    "        \"messages\": [response],\n",
    "    }"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def worker_router(state: State) -> str:\n",
    "    last_message = state[\"messages\"][-1]\n",
    "    \n",
    "    if hasattr(last_message, \"tool_calls\") and last_message.tool_calls:\n",
    "        return \"tools\"\n",
    "    else:\n",
    "        return \"evaluator\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def format_conversation(messages: List[Any]) -> str:\n",
    "    conversation = \"Conversation history:\\n\\n\"\n",
    "    for message in messages:\n",
    "        if isinstance(message, HumanMessage):\n",
    "            conversation += f\"User: {message.content}\\n\"\n",
    "        elif isinstance(message, AIMessage):\n",
    "            text = message.content or \"[Tools use]\"\n",
    "            conversation += f\"Assistant: {text}\\n\"\n",
    "    return conversation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def evaluator(state: State) -> State:\n",
    "    last_response = state[\"messages\"][-1].content\n",
    "\n",
    "    system_message = \"\"\"You are an evaluator that determines if a task has been completed successfully by an Assistant.\n",
    "Assess the Assistant's last response based on the given criteria. Respond with your feedback, and with your decision on whether the success criteria has been met,\n",
    "and whether more input is needed from the user.\"\"\"\n",
    "    \n",
    "    user_message = f\"\"\"You are evaluating a conversation between the User and Assistant. You decide what action to take based on the last response from the Assistant.\n",
    "\n",
    "The entire conversation with the assistant, with the user's original request and all replies, is:\n",
    "{format_conversation(state['messages'])}\n",
    "\n",
    "The success criteria for this assignment is:\n",
    "{state['success_criteria']}\n",
    "\n",
    "And the final response from the Assistant that you are evaluating is:\n",
    "{last_response}\n",
    "\n",
    "Respond with your feedback, and decide if the success criteria is met by this response.\n",
    "Also, decide if more user input is required, either because the assistant has a question, needs clarification, or seems to be stuck and unable to answer without help.\n",
    "\"\"\"\n",
    "    if state[\"feedback_on_work\"]:\n",
    "        user_message += f\"Also, note that in a prior attempt from the Assistant, you provided this feedback: {state['feedback_on_work']}\\n\"\n",
    "        user_message += \"If you're seeing the Assistant repeating the same mistakes, then consider responding that user input is required.\"\n",
    "    \n",
    "    evaluator_messages = [SystemMessage(content=system_message), HumanMessage(content=user_message)]\n",
    "\n",
    "    eval_result = evaluator_llm_with_output.invoke(evaluator_messages)\n",
    "    new_state = {\n",
    "        \"messages\": [{\"role\": \"assistant\", \"content\": f\"Evaluator Feedback on this answer: {eval_result.feedback}\"}],\n",
    "        \"feedback_on_work\": eval_result.feedback,\n",
    "        \"success_criteria_met\": eval_result.success_criteria_met,\n",
    "        \"user_input_needed\": eval_result.user_input_needed\n",
    "    }\n",
    "    return new_state"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def route_based_on_evaluation(state: State) -> str:\n",
    "    if state[\"success_criteria_met\"] or state[\"user_input_needed\"]:\n",
    "        return \"END\"\n",
    "    else:\n",
    "        return \"worker\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set up Graph Builder with State\n",
    "graph_builder = StateGraph(State)\n",
    "\n",
    "# Add nodes\n",
    "graph_builder.add_node(\"worker\", worker)\n",
    "graph_builder.add_node(\"tools\", ToolNode(tools=tools))\n",
    "graph_builder.add_node(\"evaluator\", evaluator)\n",
    "\n",
    "# Add edges\n",
    "graph_builder.add_conditional_edges(\"worker\", worker_router, {\"tools\": \"tools\", \"evaluator\": \"evaluator\"})\n",
    "graph_builder.add_edge(\"tools\", \"worker\")\n",
    "graph_builder.add_conditional_edges(\"evaluator\", route_based_on_evaluation, {\"worker\": \"worker\", \"END\": END})\n",
    "graph_builder.add_edge(START, \"worker\")\n",
    "\n",
    "# Compile the graph\n",
    "memory = MemorySaver()\n",
    "graph = graph_builder.compile(checkpointer=memory)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "display(Image(graph.get_graph().draw_mermaid_png()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Next comes the gradio Callback to kick off a super-step"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_thread_id() -> str:\n",
    "    return str(uuid.uuid4())\n",
    "\n",
    "\n",
    "async def process_message(message, success_criteria, history, thread):\n",
    "\n",
    "    config = {\"configurable\": {\"thread_id\": thread}}\n",
    "\n",
    "    state = {\n",
    "        \"messages\": message,\n",
    "        \"success_criteria\": success_criteria,\n",
    "        \"feedback_on_work\": None,\n",
    "        \"success_criteria_met\": False,\n",
    "        \"user_input_needed\": False\n",
    "    }\n",
    "    result = await graph.ainvoke(state, config=config)\n",
    "    user = {\"role\": \"user\", \"content\": message}\n",
    "    reply = {\"role\": \"assistant\", \"content\": result[\"messages\"][-2].content}\n",
    "    feedback = {\"role\": \"assistant\", \"content\": result[\"messages\"][-1].content}\n",
    "    return history + [user, reply, feedback]\n",
    "\n",
    "async def reset():\n",
    "    return \"\", \"\", None, make_thread_id()\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### And now launch our Sidekick UI"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "with gr.Blocks(theme=gr.themes.Default(primary_hue=\"emerald\")) as demo:\n",
    "    gr.Markdown(\"## Sidekick Personal Co-worker\")\n",
    "    thread = gr.State(make_thread_id())\n",
    "    \n",
    "    with gr.Row():\n",
    "        chatbot = gr.Chatbot(label=\"Sidekick\", height=300, type=\"messages\")\n",
    "    with gr.Group():\n",
    "        with gr.Row():\n",
    "            message = gr.Textbox(show_label=False, placeholder=\"Your request to your sidekick\")\n",
    "        with gr.Row():\n",
    "            success_criteria = gr.Textbox(show_label=False, placeholder=\"What are your success critiera?\")\n",
    "    with gr.Row():\n",
    "        reset_button = gr.Button(\"Reset\", variant=\"stop\")\n",
    "        go_button = gr.Button(\"Go!\", variant=\"primary\")\n",
    "    message.submit(process_message, [message, success_criteria, chatbot, thread], [chatbot])\n",
    "    success_criteria.submit(process_message, [message, success_criteria, chatbot, thread], [chatbot])\n",
    "    go_button.click(process_message, [message, success_criteria, chatbot, thread], [chatbot])\n",
    "    reset_button.click(reset, [], [message, success_criteria, chatbot, thread])\n",
    "\n",
    "    \n",
    "demo.launch()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<table style=\"margin: 0; text-align: left; width:100%\">\n",
    "    <tr>\n",
    "        <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
    "            <img src=\"../assets/thanks.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
    "        </td>\n",
    "        <td>\n",
    "            <h2 style=\"color:#00cc00;\">Congratulations on making the first version of Sidekick!</h2>\n",
    "            <span style=\"color:#00cc00;\">This is a pretty epic moment in the course. You've made the start of something very powerful. And you've upskilled on an impressive Agent framework in LangGraph. Maybe like me you're being converted from a LangGraph skeptic to a LangGraph fan..<br/><br/>My editor would kill me if I didn't mention again: if you're able to rate the course on Udemy, I'd be so very grateful: it's the main way that Udemy decides whether to show the course to others and it makes a massive difference.<br/><br/>And another reminder that I love <a href=\"https://www.linkedin.com/in/eddonner/\">connecting on LinkedIn</a> if you haven't yet! If you wanted to post about your progress on the course, please tag me and I'll weigh in to increase your exposure.\n",
    "            </span>\n",
    "        </td>\n",
    "    </tr>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
